[iOS] UICollectionViewで神経衰弱を作ってみた。
1 はじめに
UICollectionViewを使用すると、配列に格納されたデータを要素として、適切に改行やスペースを調整して並べてくれます。 今回は、このUICollectionViewを使用して、簡単な神経衰弱のようなアプリを作成してみました。
なお、UICollectionViewでは、レイアウトをサブクラス化して独自に設計できます。トランプが不揃いに並んでいる雰囲気は、この拡張を使用しています。
サンプルコード(http://github.com/furuya02/CardGameSample)
2 UICollectionView
プロジェクトの雛形として、UICollectionViewControllerがベースになっているものは無いので、Single View Applicationから作成し、最初のシーンを消しUICollectionViewControllerを置きました。
また、Refreshのメニューボタンを置くために、NavigationControllerをその前に置きました。
ViewControllerの基底クラスは、UICollectionViewControllerに変更しています。
#import <UIKit/UIKit.h> @interface ViewController : UICollectionViewController @end
UICollectionViewControllerは、初めから、UICollectionViewのデリゲートになっており、セルは表示されませんが、このままでもエラー(例外)は発生しません。
3 データ(トランプ)
データとなるトランプのクラスは、次のように設計されています。 保持しているデータは、「数字」(一致を検出するため)や、「表向きかどうか」などです。
@interface Card : NSObject @property int no; @property bool isFront; @property int index; -(id)initWithMark:(NSString*)mark no:(int)no; - (NSString*) imageName; - (void) Reverse:(UICollectionView *)collectionView; @end
カードの画像(名)は、内部で保持しており、「表向きかどうか」で返す名前が変わります。 また、表裏を変更するメソッドReverse:を持っており、UICollectionViewのポインタを受け取ることで、当該セルの画像を変更しています。
#import "Card.h" #import "CardCell.h" @interface Card () @property NSString *imageName; @end @implementation Card -(id)initWithMark:(NSString*)mark no:(int)no { self = [super init]; if (self != nil) { self.no = no; _imageName = [NSString stringWithFormat:@"%@%2.2d",mark,no+1]; _isFront = false; } return self; } // 画像ファイル名 - (NSString*) imageName { if ( _isFront ){ return _imageName; } return @"z02"; } // 表裏を変更する - (void) Reverse:(UICollectionView *)collectionView { _isFront ^= 1; NSIndexPath *indexPath = [NSIndexPath indexPathForItem:self.index inSection:0]; CardCell *cell = [collectionView cellForItemAtIndexPath:indexPath]; [cell.image setImage:[UIImage imageNamed:self.imageName]]; } @end
4 データソース
上記で設計したトランプクラスを配列の形で保持し、これをUICollectionViewのデータソースにしています。
<br />@property (nonatomic) NSMutableArray *cards; // アイテム数を指定する(必須) - (NSInteger) collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return self.cards.count; } //セルを返すメソッド(必須) - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{ // 再利用キューからセルを取得 CardCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"CardCell" forIndexPath:indexPath]; // セルの設定 Card *card = self.cards[indexPath.item]; [cell.image setImage:[UIImage imageNamed:card.imageName]]; cell.delegate = self; return cell; }
5 レイアウト
デフォルトのレイアウトでも、アイテムのサイズや、アイテム同士の間隔、表示のマージンなど細かい指定が可能です。
動的に変更したい場合は、それぞれで該当するデリゲートをトラップして変更することも可能です。 また、固定値でいいのであれば、StoryBoardからでも設定できます。
今回は、個々のカードを微妙に傾けたかったのですが、デフォルトのレイアウト指定では叶わないため、UICollectionViewFlowLayoutを継承した、独自のレイアウトクラスを設計しました。
#import <UIKit/UIKit.h> @interface CardLayout : UICollectionViewFlowLayout @end
設計したレイアウトクラスは、Storyboard上で、次のように指定しています。
レイアウトクラスでは、layoutAttributesForElementsInRectで全体のレイアウト属性を返しますので、ここからセルのレイアウトに関してだけを上書きしました。
#import "CardLayout.h" @implementation CardLayout // セルのレイアウト属性 - (void)setupItemAttributes:(UICollectionViewLayoutAttributes *) attributes { // 乱数で0〜30度程度傾けた int r = rand() % 4; CGFloat radian = M_PI / 180.0f * 10.0f * r; attributes.transform = CGAffineTransformMakeRotation(radian); attributes.zIndex = attributes.indexPath.item; } // 表示領域のレイアウト属性を返す - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { NSArray *allAttributes = [ super layoutAttributesForElementsInRect:rect]; for (UICollectionViewLayoutAttributes * attributes in allAttributes){ if (attributes.representedElementCategory == UICollectionElementCategoryCell){ // セルのレイアウト属性 [self setupItemAttributes:attributes]; } // 今回は、補助ビューを使用していないので、その他は省略 } return allAttributes; } @end
6 最後に
今回は、簡単なゲームのようなものを作成してみましたが、適度に調整して塩梅よく並べてくれるUICollectionViewは、結構、使い出があるような気がしてきました。 レイアウトをサブクラス化することで、細部まで調整が可能ですので、使い方によっては、面白いものが作れるかもしれません。
(トランプ画像の素材は、こちらのものを利用させて頂きました。http://sozai.7gates.net/docs/trump/)